<?php /** this is the code of phpbigtable, see http://code.google.com/p/phpbigtable/ */

// API is the one of Google App Engine Datastore http://code.google.com/appengine/docs/java/javadoc/
// Design is the one described in http://bret.appspot.com/entry/how-friendfeed-uses-mysql

// this may contain the configuration parameters (DB_NAME, etc...)
@include('phpbigtable.local.php');

/** MySQL database username */
@define('DB_USER', 'test');

/** MySQL database password */
@define('DB_PASSWORD', 'test');

/** MySQL hostname */
@define('DB_HOST', 'localhost');

/** The name of the database */
@define('DB_NAME', 'phpbigtable');

/** The max length of string properties to be indexed, as in Google Datastore */
define('MAX_INDEXABLE',500);

/** Convention to detect entity that are not yet in the datastore */
define('NOID',-1);


mysql_connect(DB_HOST, DB_USER , DB_PASSWORD)
    or die('Could not connect: ' . mysql_error());

mysql_select_db(DB_NAME) or die('Could not select database');

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Query.html */
class Query {
  var $filters = array();
  
  function addFilter($column, $type, $value) {
    $x = new _Filter();
    $this->filters[] = $x->setColumn($column)->setType($type)->setValue($value);
  }
  
  function Query($kind) {
    $this->addFilter('kind','EQUAL',$kind);
  }
}

/** internal class */
class _Filter {

  function setColumn($column) {
    $this->column = $column;
    return $this;
  }

  function setType($type) {
    $this->type = $type;
    return $this;
  }
  
  function setValue($value) {
    $this->value = $value;
    return $this;
  }
}

/** defined by Quercus {@see http://caucho.com/resin-3.1/doc/quercus.xtp#Importing classes} 
  * used to enable seamless changes between Quercus+AppEngine and PHP+MySQL */
if (!function_exists('java_class')) {
  function java_class($arg) {
    return new $arg();
  }
}
/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/DatastoreServiceFactory.html */
class DatastoreServiceFactory {
  static  $ds;
  static function getDatastoreService() {
    if (DatastoreServiceFactory::$ds == null) {
      DatastoreServiceFactory::$ds = new DatastoreService();
    }
    return DatastoreServiceFactory::$ds;
  }
}

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Query.FilterPredicate.html */
class FilterOperator {
  // see http://dev.mysql.com/doc/refman/5.1/en/non-typed-operators.html
  const EQUAL = '=';
  const GREATER_THAN = '>';
  const GREATER_THAN_OR_EQUAL = '>=';
  const IN = 'LIKE';
  const LESS_THAN = '<';
  const LESS_THAN_OR_EQUAL = '<=';
  const NOT_EQUAL = '!=';
}

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/DatastoreService.html */
class DatastoreService {
  // the list of indexes
  var $indexes;

  function DatastoreService() {
    $this->indexes = array();
    $query = 'SHOW TABLES;';
    $result = mysql_query($query) or die('Query failed: ' . mysql_error());
    $tables = array();
    while ($line = mysql_fetch_assoc($result)) {
      foreach ($line as $k => $col_value) {
        $tables[] = $col_value;
        if (preg_match('/^index_/', $col_value)) {
          $this->indexes[] = $col_value;
        }
      }
    }

    if (!in_array('entities', $tables)) {
      $init = "CREATE TABLE entities (
        id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        updated TIMESTAMP NOT NULL,
        body MEDIUMBLOB
        )  ENGINE=InnoDB;
      ";
      mysql_query($init) or die('Can not init phpbigtable ' . mysql_error());
    }
  }
  
  function delete($array_keys) {
    foreach($array_keys as $key) {
      if(!is_a($key, 'Key')) throw new Exception('$key is not a Key');
      $query = 'delete from entities where id = '.$key->getId();
      $result = mysql_query($query);
      foreach ($this->indexes as $index) {
        $query = 'delete from '.$index.' where entity_id = '.mysql_escape_string($key->getId());
        $result = mysql_query($query);
      }
    }
  } 

  function get($key) {
    if(!is_a($key, 'Key')) throw new Exception('$key is not a Key');
    $query = 'select * from entities where id = '.mysql_escape_string($key->getId());
    //echo $query;
    $result = mysql_query($query);
    $e = new Entity();
    $e->_init(mysql_fetch_assoc($result));
    return $e;
  } 

  function prepare($query) {
  
    $tables = array();
    $conditions = array();
    $onconditions = array();
    //echo print_r(DatastoreServiceFactory::getDatastoreService()->indexes); 
    foreach ($query->filters as $f) {
      $tablename = 'index_'.$f->column;
      if (in_array($tablename, DatastoreServiceFactory::getDatastoreService()->indexes)) {
        $tables[] = $tablename;
        $operator = $f->type;
        //$tmp = new FilterOperator();$sqloperator = $tmp->$operator; // does not work
        //$sqloperator = FilterOperator::$operator; // does not work
        $sqloperator = eval('return FilterOperator::'.$operator.';');
        $conditions[] =  $tablename.'.'.$f->column.$sqloperator."'".mysql_escape_string($f->value)."'";
        $onconditions[] =  $tablename.'.entity_id=entities.id';
      }
    }
    if (count($conditions)==0) {
      $where = '';
      $on = '';
    } else {
      $where = ' WHERE '.join(' and ',$conditions);
      $on = ' ON '.join(' and ',$onconditions);
    }
    
    if (count($tables)==0) { $from = 'entities'; }
    else {
      // I guess that INNER JOIN is the best, but I am not a MySQL expert
      $from = 'entities INNER JOIN '.join(' INNER JOIN ',$tables);
    }

    $query = 'select entities.id, entities.body '
              .'from '.$from.
              $on. // the ON clause, may be empty
              $where; // the WERE clause, may be empty, to get all entities
    //echo $query."<br>";
    return new PreparedQuery($query);
  }


  /** 
  If the specified Entity does not yet exist in the data store, create it and assign its Key. If the specified Entity already exists in the data store, save the new version. 
  */
  function put($entity) {
  

    // we may require a new table
    foreach($entity->data as $k=>$v) {
      if (!in_array('index_'.$k,$this->indexes) && $this->isIndexable($v)) {
        $index = mysql_escape_string ($k);
        $query = 'CREATE TABLE index_'.$index.' ( '.$index.' VARCHAR('.MAX_INDEXABLE.') NOT NULL,  entity_id INT NOT NULL,  PRIMARY KEY ('.$index.', entity_id)) ENGINE=InnoDB';
        mysql_query($query) or die('Query failed: ' . mysql_error());
        $this->indexes[] = 'index_'.$index;
      }
    }


    if ($entity->id == NOID) {
      // we create a new entity
      $query = 'INSERT INTO entities (body) VALUES ("'.mysql_escape_string(serialize($entity->data)).'");';
      $result = mysql_query($query) or die('Query failed: ' . mysql_error());
    
      // see http://www.php.net/manual/en/function.mysql-insert-id.php
      // see http://dev.mysql.com/doc/refman/5.0/en/getting-unique-id.html
      // Using LAST_INSERT_ID() and AUTO_INCREMENT columns simultaneously from multiple clients is perfectly valid. Each client will receive the last inserted ID for the last statement that client executed. 
      $id =  mysql_insert_id();
      $entity->id = $id;
      foreach($entity->data as $k=>$v) {
        if ($this->isIndexable($v)) {
          $index = mysql_escape_string ($k);
          $query = 'INSERT INTO index_'.$index.' ( '.$index.', entity_id) VALUES ("'.mysql_escape_string ($this->toIndexableValue($v)).'", '.$id.')';
          //echo $query;
          mysql_query($query) or die('Query failed: ' . mysql_error());
        }
      }
    
    } else {
      // we update the data
      //echo 'updating';
      $query = 'UPDATE entities SET body="'.mysql_escape_string (serialize($entity->data)).'" WHERE id='.$entity->id.';';
      $result = mysql_query($query) or die('update 1 failed: ' . mysql_error());
      foreach($entity->data as $k=>$v) {
            if ($this->isIndexable($v)) {
              $index = mysql_escape_string ($k);
              $query = 'UPDATE index_'.$index.' SET '.$index.'="'.mysql_escape_string ($this->toIndexableValue($v)).'" WHERE entity_id='.$entity->id.';';
              //echo $query;
              mysql_query($query) or die('update 2 failed: ' . mysql_error());
         }
       }
    }
  }

  function isIndexable($value) {
    if (is_a($value , '_Data')) return $value->isIndexable();
    return strlen($value)<MAX_INDEXABLE;
  }
  
  function toIndexableValue($value) {
    if (is_a($value , '_Data')) {
      $text = $value->serialize();
      if (strlen($text)>=MAX_INDEXABLE) { throw new Exception('Field too long to be indexed'); }
      return $text;
    }
    return $value;
  }
}

/** internal class */
abstract class _Data {
  var $_data;
  function Data($data) {
    $this->_data=$data;
  }
  function serialize() {
    return serialize($this->_data);
  }
  function isIndexable() { return true; }
}

/** internal class */
class SimpleData extends _Data {

}

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/DatastoreService.html */
class Text extends _Data {
  var $_str;
 
  function Text($data) {
    parent::Data($this);
    $this->_str = $data;
  }
  function getValue() {
    return $this->_str;
  }
  function isIndexable() { return false; }
}

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/PreparedQuery.html */
class PreparedQuery {
  var $results = array();
  
  function PreparedQuery($query) {
    //echo $query;
    $result = mysql_query($query);
    $this->num_rows = mysql_num_rows($result);
    while ($row = mysql_fetch_assoc($result)) {
      $e = new Entity();
      $e->_init($row);
      $this->results[] = $e;
      //echo $row["body"];
    }
    //echo $this->num_rows;
  }
  
  
  function asIterable() {
    return $this->results;
  } 
  function countEntities() {
    return $this->num_rows;
  }
  
  function asSingleEntity() {
    if ($this->num_rows!=1) { throw new Exception('invariant failed'); }
    return $this->results[0];
  }
}


/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Entity.html */
class Entity {
  var $data;
  var $id = NOID;
  
  /** implementation method, not part of the API */ 
  function _init($row) {
    $body = $row['body'];
    $this->data = unserialize($row["body"]);
    $this->id = $row["id"];
  }
 
  function Entity($kind='') {
    $this->data = array();
    if ($kind!='') {
      $this->setProperty('kind',$kind);
    } 
  }
  function getProperty($name) {
    if (isset($this->data[$name])) return $this->data[$name];
    return null;
  }

  function setProperty($name, $value) {
    $this->data[$name] = $value;
  }

  function getKey() {
    return new Key($this->id);
  }
} 

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/KeyFactory.html */
class KeyFactory {
  static function createKey($kind, $id) {
    return new Key($id);
  }
}

/** @see http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key.html */
class Key {
  function Key($id) {
    $this->_id = $id;
  }

  function getId() { return $this->_id; }
 
  function toString() { return $this->getId(); }
}


?>